/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package amfservices.actions;

import config.CFAchievements;
import db.PGKeys;
import config.CFCote;
import config.CFFindEggsNPC;
import config.CFMainQuests;
import config.CFMiniGame;
import config.CFPaymentType;
import config.CFPenguin;
import config.CFPlayWithFriend;
import config.CFRandomizePrize;
import config.CFReleaseEvent;
import config.CFSaleoff;
import config.CFUser;
import config.PGConfig;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import libCore.SNServices;
import logging.report.ActionRecord;
import logging.Logging;
import logging.report.UserAction;
import org.apache.commons.codec.digest.DigestUtils;
import org.json.simple.JSONValue;
import pgentity.*;
import pgentity.EntityContext;
import pgentity.events.release_event.ReleaseEventServices;
import pgentity.minigame.freakingeggs.FEServices;
import pgentity.minigame.freakingeggs.FEServices.Match;
import pgentity.minigame.freakingeggs.FreakingEggs;
import pgentity.prize.PGPrize;
import pgentity.prize.PrizeFactory;
import pgentity.quest.*;
import pgentity.services.*;
import share.*;
import share.AMFBuilder;
import share.PGException;
import zme.api.exception.ZingMeApiException;

/**
 *
 * @author KieuAnh
 */
public class PGServicesAction
{
    private PGServicesAction()
    {
        super();
    }
    
    private static final PGServicesAction inst = new PGServicesAction();
    
    public static PGServicesAction inst()
    {
        return inst;
    }
    
    public Map<String, Object> loadGameAction(String uid, String signedReq,
            long now) throws PGException, IOException, ZingMeApiException
    {
        User user;
        if (User.isExist(uid))
        {
            AutoMigrate.inst().migrate(uid);
            user = User.getUser(uid);
        }
        else
        {
            user = UserServices.inst().createNewUser(uid, signedReq, now);
        }
        
        EntityContext context = EntityContext.getContext(user);
        
        QuestServices.inst().updateQuest(context, now);
        
        QuestLogger questLogger = QuestServices.inst().getQuestLogger(uid, now);
        CoteServices.inst().updateCote(context, questLogger, now);
        
        context.saveToDB();
        
        FindEggsServices.inst().updateAndGet(user, now);
        
        UserServices.inst().registerLogin(questLogger, uid, signedReq, now);
        
        // build response
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.USER, user.buildFullAMF(true, false, true, true, now));
        
        response.put(PGMacro.PLAY_WITH_FRIEND, FriendServices.inst().buildPWFAMF(uid, now));
        
        response.put(PGMacro.QUEST,
                QuestServices.inst().buildQuestAMF(context, now));
        
        if (PGConfig.inst().releaseEvent().inEvent(now))
        {
            response.put(PGMacro.RELEASE_EVENT,
                    ReleaseEventServices.SERVICES.buildAMF(uid));
        }
        
        return response;
    }
    
    public Map<String, Object> getFriendListAction(String uid) throws PGException
    {
        FriendList friendList = FriendList.getFriendList(uid);
        return friendList.buildAMF();
    }
    
    public Map<String, Object> penguinWannaEatAction(String uid, String coteID,
            List<String> penguinIDs, long now) throws PGException
    {
        final EntityContext context = EntityContext.getContext(uid);
        
        for (String pengId : penguinIDs) {
            PGException.Assert(context.getCote().penguins().contains(pengId),
                    PGError.PENGUIN_NOT_IN_COTE, "Penguin isn't contained in cote");
        }
        
        PriorityQueue<Penguin> penguins = new PriorityQueue(penguinIDs.size(),
            new Comparator<Penguin>() {
            @Override
            public int compare(Penguin p1, Penguin p2) {
                long p1NextEatTime = PenguinServices.inst()
                        .nextEat(p1, context.getCote());
                long p2NextEatTime = PenguinServices.inst()
                        .nextEat(p2, context.getCote());
                
                return (p1NextEatTime > p2NextEatTime)?1:
                        ((p1NextEatTime == p2NextEatTime)?0:-1);
            }
        });
        
        Map<String, Object> failData = new HashMap();
        
        int remainFish = context.getCote().getPoolFish();
        Collection<Penguin> penguinEntities = Penguin
                .getPenguins(uid, coteID, penguinIDs);
        
        for (Penguin penguin : penguinEntities) {
            long nextEat = PenguinServices.inst()
                    .nextEat(penguin, context.getCote());
            if (nextEat > now)
            {
                Map<String, Object> lastPenguinEatData = new HashMap();
                lastPenguinEatData.put(PGMacro.TIME_LAST_EAT, penguin.getLastEat());
                lastPenguinEatData.put(PGMacro.FISH_LAST_EAT, penguin.getFood());
                
                failData.put(penguin.getPenguinID(), lastPenguinEatData);
            }
            else
            {
                PGException.Assert(remainFish > 0,
                        PGError.EMPTY_POOL, "Empty pool");
                PGException.Assert(PenguinServices.inst()
                        .configOf(penguin).getFeed() > 0, PGError.PENGUIN_CANNOT_EAT,
                        "Penguin cannot eat");
                
                penguins.add(penguin);
                remainFish -= Math.min(PenguinServices.inst()
                        .configOf(penguin).getFeed(), remainFish);
            }
        }
        
        List<Penguin> fedPenguins = new ArrayList(penguinIDs.size());
        while (!penguins.isEmpty())
        {
            Penguin penguin = penguins.poll();
            long nextEat = PenguinServices.inst()
                    .nextEat(penguin, context.getCote());
            
            QuestLogger questLogger = QuestServices.inst().getQuestLogger(uid, now);
            PenguinServices.inst().eat(penguin, context, questLogger, nextEat);
            fedPenguins.add(penguin);
        }
        
        Map<String, Object> successData = new HashMap();
        for (Penguin penguin : fedPenguins) {
            penguin.saveToDB();
            successData.put(penguin.getPenguinID(), AMFBuilder.make(
                    PGMacro.FISH_LAST_EAT, penguin.getFood(),
                    PGMacro.TIME_LAST_EAT, penguin.getLastEat()));
        }

        context.saveToDB();
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.SUCCESS, successData);
        response.put(PGMacro.FAIL, failData);
        
        return response;
    }
    
    public Map<String, Object> dropFishAction(String uid, String coteID, int nFish, long now) throws PGException
    {
        EntityContext context = EntityContext.getContext(uid);
        
        PGException.Assert(context.getUser().getFish() >= nFish,
                PGError.NOT_ENOUGH_FISH, "Not enough fish (" + context.getUser().getFish() + ")");
        
        QuestLogger questLogger = QuestServices.inst().getQuestLogger(uid, now);
        
        Map<String, Integer> atePenguins = CoteServices.inst().dropFish(context,
                questLogger, nFish, now);
        
        questLogger.log(new DropFishRecord());
        questLogger.log(new DropFish_FishRecord(nFish));
        
        UserServices.inst().changeFish(context.getUser(), questLogger, -nFish, now);
        //PGUserServices.inst().increaseUserExp(context, (int) (nFish * PGConst.FISH_TO_EXP), now);
        
        context.saveToDB();
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.THA_CA, nFish));
        
        return AMFBuilder.toAMF(atePenguins);
    }
    
    public Map<String, Object> upgradeCoteAction(String uid, String coteID,
            long now) throws PGException
    {
        User user =   User.getUser(uid);
        
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.NOT_USER_COTE, "Cote's not current belong to user");
        
        Cote cote   =   Cote.getCote(uid, coteID);
        
        QuestLogger userQLogger = QuestServices.inst().getQuestLogger(uid, now);
        CoteServices.inst().upgradeCote(user, userQLogger, cote, now);
        
        // update to db
        cote.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.NC_CHUONG, cote.getLevel()));
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.SUCCESS, true);
        result.put(PGMacro.LEVEL, cote.getLevel());
        return result;
    }
    
    public Map<String, Object> upgradeBoxEggAction(String uid, String coteID, long now) throws PGException
    {
        User user =   User.getUser(uid);
        
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.NOT_USER_COTE, "Cote's not current belong to user");
        
        BoxEgg boxegg = BoxEgg.getBoxEgg(uid, coteID);
        QuestLogger userQLogger = QuestServices.inst().getQuestLogger(uid, now);
        
        BoxEggServices.inst().upgradeBoxEgg(user, userQLogger, boxegg, now);
        
        // update to db
        boxegg.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.NC_TRAMCANH, boxegg.getLevel()));
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.SUCCESS, true);
        result.put(PGMacro.LEVEL, boxegg.getLevel());
        return result;
    }
    
    public Map<String, Object> sellPenguinAction(String uid, String coteID,
            String penguinId, long now) throws PGException
    {
        Cote cote   =   Cote.getCote(uid, coteID);
        PGException.Assert(cote.penguins().contains(penguinId),
                PGError.PENGUIN_NOT_IN_COTE,
                "Penguin's not contained in cote: " + coteID);
        
        Penguin penguin = Penguin.getPenguin(uid, coteID, penguinId);
        final int maxLevel = PGConfig.inst().temp().MaxPenguin_Level;
        int pLevel = penguin.getLevel();
        
        if (pLevel >= maxLevel)
        {
            boolean hasOtherMaxLvl = false;
            Collection<String> pIDs = cote.penguins().getAll();
            for (String pID : pIDs) {
                if (!pID.equals(penguinId))
                {
                    Penguin p = Penguin.getPenguin(uid, coteID, pID);
                    if (p.getLevel() >= maxLevel)
                    {
                        hasOtherMaxLvl = true;
                        break;
                    }
                }
            }
            
            PGException.Assert(hasOtherMaxLvl, PGError.CANNOT_SELL_PENGUIN,
                    "Cannot sell last lvl 10 penguin");
        }
        
        Penguin.destroy(uid, coteID, penguinId);
        cote.penguins().remove(penguinId);
        
        QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
        qLogger.log(new ParolePenguinRecord(pLevel));
        Logging.log(new ActionRecord(User.getUser(uid), now,
                UserAction.THA_CHIM, penguin.getKind(), penguin.getLevel()));
        
        //=======================================================
        // Not current implement
        // Haven't design add gold (or coin) for sell penguin
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.SUCCESS, true);
        
        return result;
    }
    
    public Map<String, Object> buyPenguinAction(long now, String uid,
            String coteID, String itemId, int nPeng) throws PGException
    {
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.NOT_FRIEND_COTE, "Cote's not current belong to user");
        
        // Check number of Peng in cote 
        Cote cote = Cote.getCote(uid, coteID);
        
        int maxPenguin = PGConfig.inst().getCote().get(cote.getLevel()).getMaxPenguin();
        PGException.Assert(cote.penguins().size() + nPeng <= maxPenguin,
                PGError.NOT_ENGOUH_PENGUIN_SLOT,
                "Cote hasn't enough slot for new penguin");
        
        User user = User.getUser(uid);
        QuestLogger userQLogger = QuestServices.inst().getQuestLogger(uid, now);
        Penguindex penguindex = Penguindex.getPenguindex(uid);
        
        // log inside
        String[] pengIds = ShopServices.inst().buyPenguins(now, user,
                userQLogger, penguindex, cote.penguins(), itemId, nPeng);
        
        user.saveToDB();
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.PENGUIN_ID_LIST, AMFBuilder.toAMF(pengIds));
        return result;
    }
    
    public Map<String, Object> buyFishAction(String userId, String itemId, long now) throws PGException
    {
        // Check user data with item value
        User user =   User.getUser(userId);
        user.updateFromDB();
        
        QuestLogger userQLogger = QuestServices.inst().getQuestLogger(userId, now);
        // log inside
        Boolean canBuy = ShopServices.inst().buyFish(user, userQLogger, itemId, now);
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.SUCCESS, canBuy);
        result.put(PGMacro.FISH, user.getFish());
        
        user.saveToDB();
        
        return result;
    }
    
    public Map<String, Object> buyGoldAction(String uid, String itemId, long now) throws PGException
    {
        // Check user data with item value
        User user =   User.getUser(uid);
        user.updateFromDB();
        
        // log inside
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        Boolean canBuy = ShopServices.inst().buyGold(user, itemId, uLogger, now);
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.SUCCESS, canBuy);
        result.put(PGMacro.GOLD, user.getGold());
        
        user.saveToDB();
        
        return result;
    }
    
    public Map<String, Object> spawnEggAction(String uid,
            String coteID, List<String> penguinIDs, long now)
            throws PGException
    {
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.INVALID_COTE, "Invalid cote");
        
        final Cote cote = Cote.getCote(uid, coteID);
        for (String pengId : penguinIDs) {
            PGException.Assert(cote.penguins().contains(pengId),
                    PGError.PENGUIN_NOT_IN_COTE, "Penguin isn't contained in cote");
        }
        
        Collection<Penguin> penguinEntities = 
                Penguin.getPenguins(uid, coteID, penguinIDs);
        
        for (Penguin penguin : penguinEntities) {
            long nextEat = PenguinServices.inst()
                    .nextEatWithoutPoolfish(penguin, cote);
            PGException.Assert(nextEat > now,
                    PGError.PENGUIN_IS_HUNGRIED, "Penguin is hungried");
        }
        
        PriorityQueue<Penguin> penguins = new PriorityQueue(Math.max(penguinIDs.size(), 1),
            new Comparator<Penguin>() {
            @Override
            public int compare(Penguin p1, Penguin p2) {
                long p1NextSpawnTime = PenguinServices.inst().nextSpawn(p1, cote);
                long p2NextSpawnTime = PenguinServices.inst().nextSpawn(p2, cote);
                
                return (p1NextSpawnTime > p2NextSpawnTime)?1:
                        ((p1NextSpawnTime == p2NextSpawnTime)?0:-1);
            }
        });
        
        Map<String, Object> failData = new HashMap();
        
        for (Penguin penguin : penguinEntities) {
            long nextSpawn = PenguinServices.inst().nextSpawn(penguin, cote);
            if (nextSpawn > now)
            {
                Map<String, Object> failPenguinData = new HashMap(2);
                failPenguinData.put(PGMacro.TIME_LAST_SPAWN, penguin.getLastSpawn());
                failPenguinData.put(PGMacro.EGG_STORE, penguin.getLastEggStorage().getValue());
                
                failData.put(penguin.getPenguinID(), failPenguinData);
            }
            else
            {
                penguins.add(penguin);
            }
        }
        
        Map<String, Object> successData = new HashMap();
        Map<String, Object> limitedEggPenguins = new HashMap();
        
        // need for add egg
        BoxEgg boxEgg = BoxEgg.getBoxEgg(uid, coteID);
        Dog dog = Dog.getDog(uid, coteID);
        
        while (!penguins.isEmpty()) {
            Penguin penguin = penguins.poll();
            long nextSpawn = PenguinServices.inst().nextSpawn(penguin, cote);
            String spawnedEggKind = PenguinServices.inst().spawnEgg(penguin, nextSpawn);

            EggStoreServices.EggStorage eggStorage = EggStoreServices.inst()
                    .addEgg(cote, boxEgg, dog, spawnedEggKind, now);
            if (eggStorage == EggStoreServices.EggStorage.LIMITED)
            {
                limitedEggPenguins.put(penguin.getPenguinID(),
                        AMFBuilder.make(PGMacro.TIME_LAST_SPAWN, penguin.getLastSpawn()));
            }
            
            penguin.setLastEggStorage(eggStorage);
            penguin.saveToDB();

            Map<String, Object> penguinResp = new HashMap();
            penguinResp.put(PGMacro.KIND, spawnedEggKind);
            penguinResp.put(PGMacro.EGG_STORE, eggStorage.getValue());
            penguinResp.put(PGMacro.TIME_LAST_SPAWN, penguin.getLastSpawn());
            successData.put(penguin.getPenguinID(), penguinResp);
        }
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.SUCCESS, successData);
        response.put(PGMacro.FAIL, failData);
        response.put(PGMacro.SPAWN_LIMITED_PENGUINS,
                AMFBuilder.toAMF(limitedEggPenguins));
        return response;
    }
    
    // Require coteID
    public Map<String, Object> moveEggFromCoteToInventoryAction(String uid,
            Map<String, Number> eggPacks, long now) throws PGException
    {
        CoteList coteList = CoteList.getCotes(uid);
        final String coteID = coteList.at(0);
        Cote cote = Cote.getCote(uid, coteID);
        Map<String, Number> validEggs = EggStoreServices.inst()
                .validateEgg(cote.eggStore(), eggPacks);
        
        Inventory inventory = Inventory.getInventory(uid);
        int inventoryAvail = Math.max(0, inventory.getMax() - inventory.numberItems());
        
        Map<String, Number> successEggs = EggStoreServices.inst()
                .truncateEgg(validEggs, inventoryAvail);
        
        int nEggMoved = 0;
        for (Map.Entry<String, Number> movedEggEntry : successEggs.entrySet()) {
            nEggMoved += movedEggEntry.getValue().intValue();
        }
        
        if (nEggMoved > 0)
        {
            EggStoreServices.inst()
                    .moveEgg(cote.eggStore(), inventory.eggStore(), successEggs);
            
            QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
            qLogger.log(new CollectEggsRecord(nEggMoved));
        }
        
        // build response amf
        Map<String, Object> response = new HashMap();
        
        // build success
        if (nEggMoved > 0)
        {
            response.put(PGMacro.SUCCESS_EGGS, successEggs);
        }
        
        // build full inventory eggs
        if (!successEggs.equals(validEggs))
        {
            Map<String, Number> fullInvEggs = EggStoreServices.inst()
                    .substractEggs(validEggs, successEggs);
            
            response.put(PGMacro.FULL_INVENTORY_EGGS, fullInvEggs);
        }
        
        // build failed eggs
        if (!validEggs.equals(eggPacks))
        {
            Map<String, Number> failedEggs = EggStoreServices.inst()
                    .substractEggs(eggPacks, validEggs);
            
            Map<String, Number> stolenEggs = EggStoreServices.inst()
                    .validateEgg(cote.stolenEggStore(), failedEggs);
            cote.stolenEggStore().removeEggs(stolenEggs);
            
            response.put(PGMacro.FAILED_EGGS, failedEggs);
        }
        
        return response;
    }
    
    // Require coteID
    public Map<String, Object> moveEggFromBoxEggToInventoryAction(String uid,
            Map<String, Number> eggPacks, final long now) throws PGException
    {
        final String coteID = CoteList.getCotes(uid).at(0);
        BoxEgg boxEgg = BoxEgg.getBoxEgg(uid, coteID);
        
        int nEggMoved = 0;
        for (Map.Entry<String, Number> movedEggEntry : eggPacks.entrySet()) {
            int nEgg = movedEggEntry.getValue().intValue();
            
            nEggMoved += nEgg;
        }
        
        Inventory inv = Inventory.getInventory(uid);
        int nEggAfterMoved = inv.eggStore().nOfEggs() + nEggMoved;
        PGException.Assert(nEggAfterMoved <= inv.getMax(),
                PGError.NOT_ENOUGH_INVENTORY_SLOT,
                String.format("Not enough inventory slot; max %d; need %d",
                        inv.getMax(), nEggAfterMoved));
        
        EggStoreServices.inst().moveEgg(boxEgg.eggStore(), inv.eggStore(), eggPacks);
        QuestLogger uQuestLogger = QuestServices.inst().getQuestLogger(uid, now);
        uQuestLogger.log(new MoveEggsFromBoxeggToInv(nEggMoved));
        
        User user = User.getUser(uid);
        Logging.log(new ActionRecord(user, now, UserAction.CHUYEN_TRUNG));
        
        return AMFBuilder.toAMF(eggPacks);
    }
    
    public Map<String, Object> sellEggsFromInventoryAction(String uid,
            Map<String, Number> soldEggPacks, long now) throws PGException
    {
        int totalEggPrice = EggStoreServices.inst().calcTotalEggsPrice(soldEggPacks);
        Inventory inv = Inventory.getInventory(uid);
        EggStoreServices.inst().removeEggFromStore(inv.eggStore(), soldEggPacks);
        
        User user = User.getUser(uid);
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        UserServices.inst().increaseGold(user, uLogger, totalEggPrice, now, UserAction.BAN_TRUNG);
        user.saveToDB();
        
        int nEggSold = 0;
        for (Map.Entry<String, Number> soldEggEntry : soldEggPacks.entrySet()) {
            int nEgg = soldEggEntry.getValue().intValue();
            
            nEggSold += nEgg;
        }
        QuestLogger logger = QuestServices.inst().getQuestLogger(uid, now);
        logger.log(new SellEggRecord(nEggSold));
        Logging.log(new ActionRecord(user, now, UserAction.BAN_TRUNG));
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.SUCCESS, true);
        return response;
    }
    
    public Map<String, Object> buyExpPenguinAction(String uid,
            String coteID, String pengId, int nExp, long now) throws PGException
    {
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.NOT_USER_COTE,
                "Cote's not contained in user cotes: " + coteID);
        
        PenguinList penguinList = PenguinList.getPenguinList(uid, coteID);
        PGException.Assert(penguinList.contains(pengId), 
                PGError.PENGUIN_NOT_IN_COTE,
                "Penguin's not contained in cote: " + pengId);
        
        User user = User.getUser(uid);
        int nReqCoin = PGConfig.inst().temp().BuyPenguinExp_Cost;
        PGException.Assert(user.getCoin() >= nReqCoin,
                PGError.NOT_ENOUGH_COIN, "Not enough coin");
        
        Penguin penguin = Penguin.getPenguin(uid, coteID, pengId);
        CFPenguin.Group grConf = PGConfig.inst().getPenguin()
                .getGroup(penguin.getKind());
        CFPenguin.Group.Level conf = grConf.get(penguin.getLevel() + 1);
        
        PGException.Assert(conf != null, PGError.MAX_LEVEL_PENGUIN,
                "Penguin's level is max");
        /*
        PGException.Assert(penguin.getExp() + nExp <= conf.getExp() ||
                grConf.containsKey(penguin.getLevel() + 2),
                PGError.MAX_LEVEL_PENGUIN,
                "Penguin's level is max");*/
        
        PenguinServices.inst().increasePenguinExp(uid, penguin, nExp, now);
        UserServices.inst().decreaseCoin(user, nReqCoin, now, UserAction.MANH_TIEN);
        
        penguin.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.MANH_TIEN,
                penguin.getKind(), penguin.getLevel(), penguin.getExp(), nReqCoin));
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.PENGUIN, penguin.buildAMF());
        Map<String, Object> userData = new HashMap();
        userData.put(PGMacro.COIN, user.getCoin());
        result.put(PGMacro.USER, userData);
        
        return result;
    }
    
    public Map<String, Object> buyLevelPenguinAction(String uid,
            String coteID, String pengId, long now) throws PGException
    {
        CoteList coteList = CoteList.getCotes(uid);
        PGException.Assert(coteList.contains(coteID),
                PGError.NOT_USER_COTE,
                "Cote's not contained in user cotes");
        
        PenguinList penguinList = PenguinList.getPenguinList(uid, coteID);
        PGException.Assert(penguinList.contains(pengId), 
                PGError.PENGUIN_NOT_IN_COTE,
                "Penguin's not contained in cote: " + pengId);
        
        Penguin penguin = Penguin.getPenguin(uid, coteID, pengId);
        CFPenguin.Group grConf = PGConfig.inst().getPenguin()
                .getGroup(penguin.getKind());
        CFPenguin.Group.Level conf = grConf.get(penguin.getLevel() + 1);
        
        PGException.Assert(conf != null, PGError.MAX_LEVEL_PENGUIN,
                "Penguin's level is max");
        
        int incExp = conf.getExp() - penguin.getExp();
        int nReqCoin = 1 +
                ((incExp - 1) / PGConfig.inst().temp().BuyPenguinLevel_ExpPerCoin);
        
        User user = User.getUser(uid);
        PGException.Assert(user.getCoin() >= nReqCoin,
                PGError.NOT_ENOUGH_COIN, "Not enough coin");
        
        PenguinServices.inst().increasePenguinExp(uid, penguin, incExp, now);
        UserServices.inst().decreaseCoin(user, nReqCoin, now, UserAction.MUA_CAP_CHIM);
        
        penguin.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.MUA_CAP_CHIM,
                penguin.getKind(), penguin.getLevel(), nReqCoin));
        
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(PGMacro.PENGUIN, penguin.buildAMF());
        Map<String, Object> userData = new HashMap();
        userData.put(PGMacro.COIN, user.getCoin());
        result.put(PGMacro.USER, userData);
        
        return result;
    }
    
    public Map<String, Object> renameCote(String uid, String coteID,
            String newName, final long now)
    {
        PGException.Assert(!PGHelper.isNullOrEmpty(newName), PGError.NULL_COTE_NAME,
                "Cote's name cannot be set to null");
        
        Cote cote = Cote.getCote(uid, coteID);
        cote.setCoteName(newName);
        cote.saveToDB();
        
        Map<String, Object> resp = new HashMap();
        resp.put(PGMacro.SUCCESS, true);
        return resp;
    }
    
    // req coteID
    public Map<String, Object> wakeDogUpAction(String uid,
            String wakeItemID, long now)
    {
        CFCote.Dog.Item wakeItem = PGConfig.inst().getCote().getDog().get(wakeItemID);
        PGException.Assert(wakeItem != null,
                PGError.INVALID_ITEM,
                "Wake item " + wakeItemID + " are invalid");
        
        User user = User.getUser(uid);
        switch (wakeItem.getPaymentType())
        {
            case CFCote.Dog.Item.PAYMENT_TYPE_GOLD:
                PGException.Assert(user.getGold() >= wakeItem.getPrice(),
                        PGError.NOT_ENOUGH_GOLD,
                        "Not enough gold (" + user.getGold() + "/" + wakeItem.getPrice() + ")");
                break;
            case CFCote.Dog.Item.PAYMENT_TYPE_COIN:
                PGException.Assert(user.getCoin() >= wakeItem.getPrice(),
                        PGError.NOT_ENOUGH_COIN,
                        "Not enough gold (" + user.getCoin() + "/" + wakeItem.getPrice() + ")");
                break;
            default:
                PGException.Assert(false,
                        PGError.UNDEFINED,
                        "Invalid payment " + wakeItem.getPaymentType());
                break;
        }
        
        final String coteID = CoteList.getCotes(uid).at(0);
        Dog dog = Dog.getDog(uid, coteID);
        final long nextSleepTime = Math.max(now, dog.getNextSleep()) + wakeItem.getTime();
        PGException.Assert(nextSleepTime <= now + PGConfig.inst().temp().MaxDogTime, 
                PGError.DOG_MAX_AWAKE_TIME, "Dog max awake time");
        
        String payType = "";
        switch (wakeItem.getPaymentType())
        {
            case CFCote.Dog.Item.PAYMENT_TYPE_GOLD:
                QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
                UserServices.inst().decreaseGold(user, qLogger, wakeItem.getPrice(),
                        now, UserAction.MUA_CHO);
                payType = "gold";
                break;
            case CFCote.Dog.Item.PAYMENT_TYPE_COIN:
                UserServices.inst().decreaseCoin(user, wakeItem.getPrice(), now,
                        UserAction.MUA_CHO);
                payType = "coin";
                break;
        }
        
        dog.setNextSleep(nextSleepTime);
        UserTempData uTempData = UserTempData.getTempData(uid);
        uTempData.setData(PGMacro.WAKE_DOG_FIRST_TIME, true);
        
        dog.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.MUA_CHO, payType, wakeItem.getPrice()));
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.NEXT_SLEEP, nextSleepTime);
        return response;
    }
    
    // req coteID
    public Map<String, Object> wakeDogUpByGoldAction(String uid, long now)
    {
        final String coteID = CoteList.getCotes(uid).at(0);
        Cote cote = Cote.getCote(uid, coteID);
        final int gold = PGConfig.inst().getCote().get(cote.getLevel()).getDogGold();
        
        User user = User.getUser(uid);
        PGException.Assert(user.getGold() >= gold,
                PGError.NOT_ENOUGH_GOLD,
                "Not enough gold (" + user.getGold() + "/" + gold + ")");
        
        Dog dog = Dog.getDog(uid, coteID);
        final long nextSleepTime = Math.max(now, dog.getNextSleep()) + TimeUtil.DAY_SECS * 1000L;
        PGException.Assert(nextSleepTime <= now + PGConfig.inst().temp().MaxDogTime, 
                PGError.DOG_MAX_AWAKE_TIME, "Dog max awake time");
        
        String payType = "gold";
        QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
        UserServices.inst().decreaseGold(user, qLogger, gold,
                now, UserAction.MUA_CHO);
        
        dog.setNextSleep(nextSleepTime);
        UserTempData uTempData = UserTempData.getTempData(uid);
        uTempData.setData(PGMacro.WAKE_DOG_FIRST_TIME, true);
        
        dog.saveToDB();
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.MUA_CHO, payType, gold));
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.NEXT_SLEEP, nextSleepTime);
        return response;
    }
    
    public Map<String, Object> acceptDailyQuestAction(String uid, int questIndex, long now)
    {
        DailyQuest dailyQuest = DailyQuest.getQuest(uid, now);
        PGException.Assert(dailyQuest.getCurrentIndex() == questIndex,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "Quest " + questIndex + " doesn't current; current is "
                        + dailyQuest.getCurrentIndex());
        
        PGException.Assert(dailyQuest.getCurrentState() == QuestState.NEW,
                PGError.ACCEPT_NOT_NEW_QUEST,
                "Quest " + questIndex + " must be new; current state: "
                        + dailyQuest.getCurrentState());
        
        EntityContext context = EntityContext.getContext(uid);
        QuestServices.inst().acceptDailyQuest(dailyQuest, context, now);
        
        context.saveToDB();
        dailyQuest.saveToDB();
        
        int cost = dailyQuest.getCost(context.getUser().getLevel());
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.NHAN_Q_HN,
                cost, dailyQuest.getCurrentIndex()));
        
        Map<String, Object> response = new HashMap();
        response.put(PGMacro.SUCCESS, true);
        return response;
    }
    
    public Map<String, Object> returnDailyQuestAction(String uid, int questIndex, long now)
    {
        DailyQuest dailyQuest = DailyQuest.getQuest(uid, now);
        PGException.Assert(dailyQuest.getCurrentIndex() == questIndex,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "Quest " + questIndex + " doesn't current; current is "
                        + dailyQuest.getCurrentIndex());
        
        PGException.Assert(dailyQuest.getCurrentState()== QuestState.ACCEPTED,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "Quest " + questIndex + " must be accepted for return; current state: "
                        + dailyQuest.getCurrentState());
        
        EntityContext context = EntityContext.getContext(uid);
        Object prize = dailyQuest.getCurrentPrize(context.getUser().getLevel());
        Map<String, Object> pzDesc = QuestServices.inst().returnDailyQuest(dailyQuest, context, now);
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.TRA_Q_HN, questIndex));
        
        context.saveToDB();
        dailyQuest.saveToDB();
        
        return pzDesc;
    }
    
    public Map<String, Object> completeDailyQuestImmediately(String uid, int questIndex, long now)
    {
        DailyQuest dailyQuest = DailyQuest.getQuest(uid, now);
        PGException.Assert(dailyQuest.getCurrentIndex() == questIndex,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "Quest " + questIndex + " doesn't current; current is "
                        + dailyQuest.getCurrentIndex());
        
        PGException.Assert(dailyQuest.getCurrentState()== QuestState.ACCEPTED,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "Quest " + questIndex + " must be accepted for complete; current state: "
                        + dailyQuest.getCurrentState());
        
        User user = User.getUser(uid);
        final int completeImmCost = PGConfig.inst().getDailyQuest()
                .get(questIndex).getCompleteImmCost(user.getLevel());
        
        PGException.Assert(user.getCoin() >= completeImmCost,
                PGError.NOT_ENOUGH_COIN,
                "Not enough coin for complete quest immedately; Require: " +
                user.getCoin() + " have: " + completeImmCost);
        
        EntityContext context = EntityContext.getContext(user);
        Map<String, Object> pzDesc = QuestServices.inst()
                .completeImmediatelyDailyQuest(dailyQuest, context, now);
        
        UserServices.inst().decreaseCoin(user, completeImmCost, now,
                UserAction.HT_NHANH_Q_HN);
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.HT_NHANH_Q_HN,
                completeImmCost, questIndex));
        
        context.saveToDB();
        dailyQuest.saveToDB();
        
        return pzDesc;
    }
    
    public Map<String, Object> returnMainQuestAction(String uid, String qLine, long now)
    {
        MainQuestLine questLine = MainQuestLine.getQuestLine(uid, qLine);
        final int acceptLvl = questLine.getLastAcceptLevel();
        final int qIndex = questLine.getIndex();
        
        PGException.Assert(questLine.getState() == QuestState.ACCEPTED,
                PGError.QUEST_WAS_NOT_ACCEPTED,
                "You haven't accepted quest " + qLine + "{" + questLine.getIndex() +"} yet!");
        
        EntityContext context = EntityContext.getContext(uid);
        PGException.Assert(questLine.getChecker().isAccept(context),
                PGError.INCOMPLETED_QUEST,
                "You not have enough resource for return this quest");
        
        questLine.getChecker().returnQuest(context);
        Map<String, Object> prizeDesc = questLine.getPrize().award(context, now);
        
        QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
        qLogger.log(new CompletedMainQuestRecord());
        
        questLine.setState(QuestState.RETURNED);
        
        CFMainQuests.QuestLine qLineConf = PGConfig.inst().getMainQuest().get(qLine);
        
        // get new main quest
        int currentMinLevel = qLineConf.minimizeLevel(context.getUser().getLevel());
        int lastAcceptMinLevel = qLineConf.minimizeLevel(questLine.getLastAcceptLevel());
        if (currentMinLevel > lastAcceptMinLevel)
        {
            questLine.setIndex(0);
            questLine.setLastAcceptLevel(context.getUser().getLevel());
            questLine.setState(QuestState.ACCEPTED);
        }
        else if (questLine.getIndex() + 1 < qLineConf.get(currentMinLevel).size())
        {
            questLine.setIndex(questLine.getIndex() + 1);
            questLine.setLastAcceptLevel(context.getUser().getLevel());
            questLine.setState(QuestState.ACCEPTED);
        }
        
        context.saveToDB();
        questLine.saveToDB();
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.TRA_Q_DH, qLine, qIndex, acceptLvl));
        
        return prizeDesc;
    }
    
    public Map<String, Object> takeSnapshotAction(String uid, long now)
    {
        UserDailyData uDailyData = UserDailyData.getData(uid, now);
        boolean isTakenSnapshot = Boolean.parseBoolean(
                uDailyData.getData(PGMacro.TAKEN_SNAPSHOT));
        User user = User.getUser(uid);
        if (!isTakenSnapshot)
        {
            int nCoinInc = PGConfig.inst().temp().TakeSnapshotFirstTimeInDay_Prize;
            UserServices.inst().increaseCoin(user, nCoinInc, now, UserAction.CHUP_HINH);
            user.saveToDB();
            uDailyData.setData(PGMacro.TAKEN_SNAPSHOT, true);
            Logging.log(new ActionRecord(user, now, UserAction.CHUP_HINH, nCoinInc));
        }
        
        QuestLogger questLogger = QuestServices.inst().getQuestLogger(uid, now);
        TakeSnapshotQuestRecord qRecord = new TakeSnapshotQuestRecord();
        questLogger.log(qRecord);
        
        Map<String, Object> resp = new HashMap(1);
        resp.put(PGMacro.TAKEN_SNAPSHOT, isTakenSnapshot);
        return resp;
    }
    
    public Map<String, Object> setUIStateAction(String uid, Map<String, Number> tuts)
    {
        UIData tut = UIData.getEntity(uid);
        tut.setStates(tuts);
        
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> saveSettingsAction(String uid, Map<String, Object> settings)
    {
        UserSettings userSettings = UserSettings.getEntity(uid);
        userSettings.setStates((Map) settings);
        
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> getMailsAction(String uid, int offset, int length)
    {
        Mailbox3 mailbox = Mailbox3.getMails(uid);
        mailbox.resetUnreadMail();
        return mailbox.buildAMF(offset, length);
    }
    
    public Map<String, Object> clearAllMailsAction(String uid)
    {
        Mailbox3.destroy(uid);
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> getUserGiftsAction(String uid)
    {
        UserGifts userGifts = UserGifts.getGift(uid);
        return userGifts.buildAMF();
    }
    
    public Map<String, Object> receiveGiftAction(String uid, String giftID,
            long now)
    {
        UserGifts userGifts = UserGifts.getGift(uid);
        PGException.Assert(userGifts.contains(giftID), PGError.INVALID_GIFT,
                "Gift " + giftID + " not be " + uid + "'s gift");
        
        Gift gift = Gift.getGift(giftID);
        PGPrize giftPrize = PrizeFactory.getPrize(gift.getPrizeData(), UserAction.NHAN_GOI_QUA);
        
        EntityContext context = EntityContext.getContext(uid);
        Map<String, Object> prizeDesc = giftPrize.award(context, now);
        
        context.saveToDB();
        userGifts.remove(giftID);
        
        String pzData = JSONValue.toJSONString(gift.getPrizeData());
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.NHAN_GOI_QUA, pzData));
        
        return prizeDesc;
    }
    
    public Map<String, Object> destroyGiftAction(String uid, String giftID,
            long now)
    {
        UserGifts userGifts = UserGifts.getGift(uid);
        PGException.Assert(userGifts.contains(giftID), PGError.INVALID_GIFT,
                "Gift " + giftID + " not be " + uid + "'s gift");
        
        Gift gift = Gift.getGift(giftID);
        
        userGifts.remove(giftID);
        Gift.destroyGift(giftID);
        
        String pzData = JSONValue.toJSONString(gift.getPrizeData());
        Logging.log(new ActionRecord(User.getUser(uid), now,
                UserAction.HUY_GOI_QUA, pzData));
        
        return Collections.EMPTY_MAP;
    }
    
    // [UNUSED]
    public Map<String, Object> sendGiftAction(List<String> receivers,
            Map<String, Object> giftPrize, long now, int expired)
    {
        Gift gift  = GiftServices.inst()
                .sendGift(receivers, giftPrize, now, expired);
        
        Map<String, Object> resp = new HashMap();
        resp.put(PGMacro.GIFT_ID, gift.getGiftID());
        return resp;
    }
    
    // [UNUSED]
    public Map<String, Object> sendGiftToAllUsersAction(
            Map<String, Object> giftPrize, long now, int expired)
    {
        Collection<String> allUsers = UserServices.inst().getAllUsers();
        
        Gift gift = GiftServices.inst()
                .sendGift(allUsers, giftPrize, now, expired);
        
        Map<String, Object> resp = new HashMap();
        resp.put(PGMacro.GIFT_ID, gift.getGiftID());
        return resp;
    }
    
    public Map<String, Object> loginAwardAction(String uid, long now)
    {
        UserDailyData uDailyData = UserDailyData.getData(uid, now);
        PGException.Assert(
                !Boolean.parseBoolean(uDailyData.getData(PGMacro.RECEIVED_LOGIN_PRIZE)),
                PGError.RECEIVED_LOGIN_PRIZE, "You have received this prize");
        
        UserTempData uTempData = UserTempData.getTempData(uid);
        int nRepLoginDay = PGHelper.toInteger(
                uTempData.getData(PGMacro.REPEATED_LOGIN_DAY));
        
        // prizing
        Map<String, Object> prizeDesc = Collections.EMPTY_MAP;
        if (nRepLoginDay > 0)
        {
            PGPrize dailyLoginPrize = PrizeFactory.getPrize(PGConfig.inst()
                    .getPrizing().dailyLogin().prizeForDay(nRepLoginDay), UserAction.QUA_DANG_NHAP_HN);
            
            EntityContext context = EntityContext.getContext(uid);
            prizeDesc = dailyLoginPrize.award(context, now);
            context.saveToDB();
        }
        
        uDailyData.setData(PGMacro.RECEIVED_LOGIN_PRIZE, true);
        
        Logging.log(new ActionRecord(User.getUser(uid), now,
                UserAction.QUA_DANG_NHAP_HN, nRepLoginDay));
        
        Map<String, Object> resp = new HashMap(2);
        resp.put(PGMacro.REPEATED_LOGIN_DAY, nRepLoginDay);
        resp.put(PGMacro.PRIZE, prizeDesc);
        return resp;
    }
    
    public Map<String, Object> getAchievemensAction(String uid, long now)
    {
        EntityContext context = EntityContext.getContext(uid);
        return QuestServices.inst().buildAchievementsAMF(context, now);
    }
    
    public Map<String, Object> receiveAchievementPrizeAction(String uid,
            String achID, String medalID, long now)
    {
        CFAchievements.Achievement conf = PGConfig.inst()
                .getAchievements().get(achID);
        PGException.Assert(conf.isEnable(),
                PGError.ACHIEVEMENT_DISABLED, "Achievement are disabled");
        
        Achievement achivement = Achievement.getAchievements(uid, achID);
        EntityContext context = EntityContext.getContext(uid);
        
        PGException.Assert(!achivement.isReceivedPrize(medalID),
                PGError.RECEIVED_ACHIEVEMENT, "You have received this prize");
        PGException.Assert(achivement.getChecker(medalID).isAccept(context),
                PGError.INCOMPLETED_BEFORE_QUEST, "Incompleted achivement");
        
        Map<String, Object> prizeDesc = achivement.getPrize(medalID).award(context, now);
        context.saveToDB();
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.NHAN_THANH_TUU, achID, medalID));
        
        achivement.setReceivedPrize(medalID);
        return prizeDesc;
    }
    
    public Map<String, Object> upgradeAchievementStar(String uid,
            String ach, int nextStar, long now)
    {
        CFAchievements.Achievement conf = PGConfig.inst()
                .getAchievements().get(ach);
        PGException.Assert(conf.isEnable(),
                PGError.ACHIEVEMENT_DISABLED, "Achievement are disabled");
        
        Achievement achivement = Achievement.getAchievements(uid, ach);
        int star = achivement.star();
        
        PGException.Assert(nextStar == star + 1, PGError.INVALID_ACHIEVEMENT_STAR,
                "Invalid star");
        PGException.Assert(conf.stars().containsKey(nextStar),
                PGError.ACHIEVEMENT_MAX_STAR, "Max star");
        
        EntityContext context = EntityContext.getContext(uid);
        PGException.Assert(achivement.getChecker(conf.getMaxMedal(star)).isAccept(context),
                PGError.INCOMPLETED_BEFORE_QUEST, "Incompleted last achivement");
        
        List<String> receivers = Arrays.asList(new String[] {uid});
        Map<String, Object> resp = new HashMap();
        for (String medal : conf.stars().get(star)) {
            if (!achivement.isReceivedPrize(medal))
            {
                //achivement.getPrize(medalID).award(context, now);
                Map<String, Object> prize = conf.get(medal).getPrize();
                Gift gift = GiftServices.inst().sendGift(receivers, prize,
                        now, GiftServices.INFINITI_EXPIRE);
                
                achivement.setReceivedPrize(medal);
                resp.put(medal, gift.buildIDAMF());
            }
        }
        
        achivement.setStar(nextStar);
        
        Logging.log(new ActionRecord(context.getUser(), now,
                UserAction.LEN_SAO_THANH_TUU, ach, nextStar));
        
        return resp;
    }
    
    public Map<String, Object> takeRandomizePrizeAction(String uid, long now)
    {
        UserTempData uTempData = UserTempData.getTempData(uid);
        
        CFRandomizePrize.Type rpType = CFRandomizePrize.Type.RP2;
        int nTurn = PGHelper.toInt(uTempData.getData(rpType.field()));
        if (nTurn <= 0)
        {
            rpType = CFRandomizePrize.Type.RP1;
            nTurn = PGHelper.toInt(uTempData.getData(rpType.field()));
        }
        PGException.Assert(nTurn > 0, PGError.NOT_ENOUGH_RP_TURN, "You have 0 turn");
        
        // reduce turn
        --nTurn;
        uTempData.setData(rpType.field(), nTurn);
        
        String prizeID = PGConfig.inst().getRandomizePrizes().randomPrize(rpType);
        
        CFRandomizePrize.Prize prizeData = PGConfig.inst()
                .getRandomizePrizes().get(prizeID);
        
        User user = User.getUser(uid);
        Logging.log(new ActionRecord(user, now, UserAction.QUAY_SO, prizeID));
        if (prizeData.isAutoPrize())
        {
            PGPrize prize = PrizeFactory.getPrize(prizeData.getPrize(), UserAction.QUAY_SO);
            EntityContext context = EntityContext.getContext(user);
            Object pzDesc = prize.award(context, now);
            context.saveToDB();
            
            // find total gold prized:
            Deque<Map<String, Object>> pzStack = new ArrayDeque();
            int totalGoldPrized = 0;
            pzStack.add(prizeData.getPrize());
            while (!pzStack.isEmpty())
            {
                Map<String, Object> pz = pzStack.pollLast();
                for (Map.Entry<String, Object> pzEntry : pz.entrySet()) {
                    String pzKey = pzEntry.getKey();
                    Object pzVal = pzEntry.getValue();
                    
                    if (pzVal instanceof Map)
                    {
                        pzStack.addLast((Map) pzVal);
                    }
                    else if ("gold".equals(pzKey))
                    {
                        totalGoldPrized += PGHelper.toInteger(pzVal);
                    }
                }
            }
            
            if (totalGoldPrized > 0)
            {
                QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
                qLogger.log(new GoldDialRecord(totalGoldPrized));
            }
            
            return AMFBuilder.make(PGMacro.RAND_PRIZE_ID, prizeID,
                    PGMacro.PRIZE, pzDesc);
        }
        else
        {
            Gift gift = GiftServices.inst().sendGift(Arrays.asList(new String[] {uid}),
                    prizeData.getPrize(), now, PGConfig.inst().temp().RandomizePrize_Expire);
            
            return AMFBuilder.make(PGMacro.RAND_PRIZE_ID, prizeID, PGMacro.GIFT, gift.buildIDAMF());
        }
    }
    
    public Map<String, Object> buyRandomizePrizeTurnAction(String uid, String itemID, long now)
    {
        CFRandomizePrize.Items itemsConf = PGConfig.inst().getRandomizePrizes().getItems();
        CFRandomizePrize.Items.Item item = itemsConf.get(itemID);
        PGException.Assert(item != null, 
                PGError.INVALID_ITEM, "Invalid item");
        
        int priceVal = itemsConf.getPrice(itemID);
        CFSaleoff.Sale sale = PGConfig.inst().getSaleoff().getSale(now);
        if (sale != null && sale.getRpItems().containsKey(itemID))
        {
            priceVal = sale.getRpItems().get(itemID);
        }
        
        User user = User.getUser(uid);
        
        UserDailyData uDaily = UserDailyData.getData(uid, now);
        if (PGMacro.GOLD.equals(item.getPaymentType()))
        {
            int nTurnHaveBought = PGHelper.toInteger(PGMacro.RP_TURN_BOUGHT_BY_GOLD);
            
            PGException.Assert(nTurnHaveBought < PGConfig.inst().temp()
                    .MaxBuyRPByGoldPerDay, PGError.CANNOT_BUY_RP_TURN,
                    "You have bought maximum turn by gold in day");
            
            PGException.Assert(user.getGold() >= priceVal,
                    PGError.NOT_ENOUGH_GOLD, "Not enough gold");
            
            QuestLogger uQuestLogger = QuestServices.inst().getQuestLogger(uid, now);
            UserServices.inst().decreaseGold(user, uQuestLogger, priceVal,
                    now, UserAction.MUA_QUAY_SO);
            uDaily.incData(PGMacro.RP_TURN_BOUGHT_BY_GOLD, item.getnTurn());
        }
        else if (PGMacro.COIN.equals(item.getPaymentType()))
        {
            int nTurnHaveBought = PGHelper.toInteger(PGMacro.RP_TURN_BOUGHT_BY_COIN);
            
            PGException.Assert(nTurnHaveBought < PGConfig.inst().temp()
                    .MaxBuyRPByCoinPerDay, PGError.CANNOT_BUY_RP_TURN,
                    "You have bought maximum turn by coin in day");
            
            PGException.Assert(user.getCoin() >= priceVal,
                    PGError.NOT_ENOUGH_COIN, "Not enough coin");
            
            UserServices.inst().decreaseCoin(user, priceVal, now,
                    UserAction.MUA_QUAY_SO);
            uDaily.incData(PGMacro.RP_TURN_BOUGHT_BY_COIN, item.getnTurn());
        }
        else
        {
            PGException.Assert(false, PGError.INVALID_ITEM,
                    "Invalid payment " + item.getPaymentType());
        }
        
        UserTempData uTempData = UserTempData.getTempData(uid);
        int nTurn = PGHelper.toInteger(uTempData.getData(PGMacro.RAND_PRIZE_TURN));
        nTurn += item.getnTurn();
        uTempData.setData(PGMacro.RAND_PRIZE_TURN, nTurn);
        
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.MUA_QUAY_SO,
                item.getPaymentType(), priceVal, item.getnTurn()));
        
        return AMFBuilder.make(PGMacro.RAND_PRIZE_TURN, nTurn);
    }
    
    public Map<String, Object> reloadFriendListAction(String uid,
            String signedRequest, long now)
            throws ZingMeApiException, IOException
    {
        UserTempData uTempData = UserTempData.getTempData(uid);
        long lastSync = PGHelper.toLong(
                uTempData.getData(PGMacro.LAST_TIME_SYNC_FRIEND_LIST));
        
        PGException.Assert(lastSync + PGConfig.inst().temp().SyncFriendsCooldown <
                now, PGError.SYNC_FRIEND_ARE_LOCKED,
                "Sync friend are locked (unlock - " +
                lastSync + PGConfig.inst().temp().SyncFriendsCooldown + ")");
        
        SNServices sns = new SNServices(signedRequest);
        PGException.Assert(sns.validUser(uid),
                PGError.INVALID_SIGNED_REQUEST, "Signed request are invalid");
        FriendList friendList = FriendList.getFriendList(uid);
        
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        FriendServices.inst().reloadFriendList(friendList, sns, uLogger, uTempData, now);
        
        return friendList.buildAMF();
    }
    
    public Map<String, Object> wakeDogFirstTimeAction(String uid, String coteID,
            long now)
    {
        UserTempData uTempData = UserTempData.getTempData(uid);
        boolean isWakenDogFirstTime = Boolean.parseBoolean(
                uTempData.getData(PGMacro.WAKE_DOG_FIRST_TIME));
        PGException.Assert(!isWakenDogFirstTime, PGError.HAVE_WAKEN_DOG_FIRST_TIME,
                "You have waken dog first time");
        
        Dog dog = Dog.getDog(uid, coteID);
        PGException.Assert(!dog.isAwake(now), PGError.HAVE_WAKEN_DOG_FIRST_TIME,
                "The dog is awaken right now");
        
        final long nextSleepTime = Math.max(now, dog.getNextSleep())
                + PGConfig.inst().temp().WakeDogFirstTime_Time;
        PGException.Assert(nextSleepTime <= now + PGConfig.inst().temp().MaxDogTime, 
                PGError.DOG_MAX_AWAKE_TIME, "Dog max awake time");
        
        dog.setNextSleep(nextSleepTime);
        uTempData.setData(PGMacro.WAKE_DOG_FIRST_TIME, true);
        dog.saveToDB();
        
        User user = User.getUser(uid);
        Logging.log(new ActionRecord(user, now, UserAction.MUA_CHO,
                "first_time_free", 0));
        
        Map<String, Object> resp = new HashMap(1);
        resp.put(PGMacro.NEXT_SLEEP, nextSleepTime);
        return resp;
    }
    
    public Map<String, Object> takeAdsAction(String uid, long now)
    {
        UserDailyData uDailyData = UserDailyData.getData(uid, now);
        boolean isTakenAds = Boolean.parseBoolean(
                uDailyData.getData(PGMacro.TAKEN_ADS));
        PGException.Assert(!isTakenAds, PGError.HAVE_TAKEN_ADS_TODAY,
                "You have taken ads today");
        
        uDailyData.setData(PGMacro.TAKEN_ADS, true);
            
        EntityContext context = EntityContext.getContext(uid);

        Map<String, Object> prizeData = PGConfig.inst()
                .getPrizing().getAds().getPrize(context.getUser().getLevel());
        PGPrize prize = PrizeFactory.getPrize(prizeData, UserAction.QUANG_CAO);
        Map<String, Object> prizeDesc = prize.award(context, now);
        context.saveToDB();
        
        Logging.log(new ActionRecord(User.getUser(uid), now, UserAction.QUANG_CAO));

        return prizeDesc;
    }
    
    @Deprecated
    public Map<String, Object> makeGiftCodes(Map<String, Object> giftData,
            int giftExpire, int nCode, int remain, int codeExpire, long now)
    {
        PGException.Assert(false, PGError.UNDEFINED, "Deprecated service");
        final String templID = PGKeys.randomKey();
        GiftTemplate.newTemplate(templID, giftData, giftExpire, codeExpire);
        
        List<String> codes = new ArrayList(nCode);
        for (int i = 0; i < nCode; i++) {
            do
            {
                String code = PGKeys.randomCode();
                if (!GiftCode.isExist(code))
                {
                    GiftCode.newCode(code, templID, remain, codeExpire);
                    codes.add(code);
                    break;
                }
            } while(true);
        }
        
        return AMFBuilder.toAMF(codes);
    }
    
    public Map<String, Object> useGiftCode(String uid, String code, long now)
    {
        code = code.trim().toUpperCase();
        try
        {
            GiftCode giftCode = GiftCode.getGift(code);
            UsedGiftCode usedGC = UsedGiftCode.getUsedGiftCode(uid);
            
            PGException.Assert(giftCode.getRemain() > 0, PGError.INVALID_GIFT_CODE, "");
            PGException.Assert(!usedGC.contains(code), PGError.INVALID_GIFT_CODE, "");
            PGException.Assert(!(giftCode.isGrouped() &&
                    usedGC.contains(giftCode.getGroup())), PGError.INVALID_GIFT_CODE, "");
            
            GiftTemplate giftTemplate = GiftTemplate
                    .getTemplate(giftCode.getGiftTemplateID());

            List<String> receivers = new ArrayList(1);
            receivers.add(uid);

            Map<String, Object> giftPrize = (Map)
                    JSONValue.parse(giftTemplate.getGiftData());

            Gift gift = GiftServices.inst()
                    .sendGift(receivers, giftPrize, now, giftTemplate.getGiftExpire());
            
            if (giftCode.isGrouped())
            {
                usedGC.add(code, giftCode.getGroup());
            }
            else
            {
                usedGC.add(code);
            }

            giftCode.use();
            giftCode.saveToDB();

            Logging.log(new ActionRecord(User.getUser(uid), now,
                    UserAction.NHAP_GIFT_CODE, giftCode));

            return gift.buildAMF();
        }
        catch (Exception ex)
        {
        }
        
        Map<String, Object> resp = new HashMap(1);
        resp.put(PGMacro.ERROR_CODE, PGError.INVALID_GIFT_CODE);
        return resp;
    } 
    
    public Map<String, Object> getPaymentAction(String uid, String billNo)
    {
        Payment payment = Payment.getPayment(billNo);
        PGException.Assert((uid != null) && uid.equals(payment.getUid()),
                PGError.BILL_NO_NOT_BELONG_TO_USER,
                "Billno " + billNo + " not belong to user " + uid);
        
        return payment.buildAMF();
    }
    
    public Map<String, Object> addWhiteListAction(List<String> whiteList)
    {
        String[] whiteArr = whiteList.toArray(new String[whiteList.size()]);
        UserList.getList(UserList.ListType.WHITE_LIST).add(whiteArr);
        
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> addSystemListAction(List<String> sysList)
    {
        String[] sysArr = sysList.toArray(new String[sysList.size()]);
        UserList.getList(UserList.ListType.SYSTEM_ACCOUNT).add(sysArr);
        
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> deleteUserAction(String uid, String adminPassword)
    {
        String md5Pass = DigestUtils.md5Hex(adminPassword);
        PGException.Assert(PGConfig.inst().temp().SystemPasswordMD5.equals(md5Pass),
                PGError.INVALID_SIGNED_REQUEST, "Invalid password");
        
        try
        {
            UserServices.inst().destroyUser(uid);
        }
        catch (Exception ex)
        {
            PGException.pgThrow(ex);
        }
        
        return Collections.EMPTY_MAP;
    }
    
    public Map<String, Object> getGameMessagesAction(long now)
    {
        return GameMessageList.getMessages().buildAMF(true);
    }
    
    public Map<String, Object> getAllGameMessagesAction(long now)
    {
        return GameMessageList.getMessages().buildAMF(false);
    }
    
    public Map<String, Object> addGameMessagesAction(List<Map<String, Object>> data, long now)
    {
        String[] msgIDs = new String[data.size()];
        int i = 0;
        for (Map<String, Object> msgData : data) {
            String content = (String) msgData.get("content");
            int order = PGHelper.toInteger(msgData.get("order"));
            int expire = PGHelper.toInteger(msgData.get("expire"));
            
            String msgID = PGKeys.randomKey();
            GameMessage.newMsg(msgID, content, order, expire);
            msgIDs[i++] = msgID;
        }
        
        GameMessageList.getMessages().add(msgIDs);
        
        return Collections.EMPTY_MAP;
    }
    
    public Object visitCote(String uid, String coteID, long now)
    {
        EntityContext context = EntityContext.getContext(uid, coteID);
        QuestLogger uQLogger = QuestServices.inst().getQuestLogger(uid, now);
        CoteServices.inst().updateCote(context, uQLogger, now);
        context.saveToDB();
        
        return CoteServices.inst().buildCoteAMF(context.getCote(),
                true, true, true, true);
    }
    
    public Map<String, Object> harvestSnowman(String uid, String coteID, long now)
    {
        CFPlayWithFriend.Snowman conf = PGConfig.inst().getFriend()
                .snowman();
        
        Snowman snowman = Snowman.getEntity(uid);
        PGException.Assert(snowman.currentStep() >= conf.maxStep(),
                PGError.INCOMPLETED_SNOW_MAN, "");
        
        int prizeIndex = conf.getHarvestPrizes().randomPrize();
        Map<String, Object> prizeData = conf.getHarvestPrizes().getPrize(prizeIndex).get();
        PGPrize prize = PrizeFactory.getPrize(prizeData, UserAction.THU_HOACH_NG_TUYET);
        
        snowman.reset();
        
        // prizing
        EntityContext context = EntityContext.getContext(uid);
        Object pzDesc = prize.award(context, now);
        context.saveToDB();
        
        QuestLogger logger = QuestServices.inst().getQuestLogger(uid, now);
        logger.log(new HarvestSnowmanRecord());
        
        Map<String, Object> ret = AMFBuilder.make("prize", prizeIndex, "desc", pzDesc);
        
        CFReleaseEvent reConfig = PGConfig.inst().releaseEvent();
        String bonusItem = "";
        if (reConfig.inEvent(now))
        {
            bonusItem = conf.randomBonus();
            ReleaseEventServices.SERVICES.addItemToCote(context.getCote(), bonusItem);
            ret.put("bonus", bonusItem);
        }
        
        Logging.log(new ActionRecord(context.getUser(), now, UserAction.THU_HOACH_NG_TUYET, prizeIndex, bonusItem));
        
        return ret;
    }
    
    public Object receiveCoinCardAction(String uid, long now)
    {
        CoinCard cc = CoinCard.getCC(uid);
        PGException.Assert(cc.inCC(now),
                PGError.NOT_IN_CC, "You are not in cc time");
        
        UserDailyData dailyData = UserDailyData.getData(uid, now);
        boolean isReceived = PGHelper.toBoolean(dailyData.getData(PGMacro.COIN_CARD));
        PGException.Assert(!isReceived, PGError.ALREADY_RECEIVED_CC,
                "You already recevied cc today");
        
        dailyData.setData(PGMacro.COIN_CARD, true);
        
        int incCoin = PGConfig.inst().temp().CoinCardCoinPerDay;
        User user = User.getUser(uid);
        UserServices.inst().increaseCoin(user, incCoin, now, UserAction.NHAN_THE_THANG);
        user.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.NHAN_THE_THANG));
        
        return AMFBuilder.make(PGMacro.COIN, user.getCoin());
    }
    
    public Object playFreakingEggsAction(String uid, long now)
    {
        Minigame minigame = Minigame.getMinigame(uid);
        int turn = minigame.getTurn();
        PGException.Assert(turn > 0, PGError.NOT_ENOUGH_MNG_TURN,
                "Not enough turn");
        
        try {
            List<Match> matches = FEServices.inst().generate(0, 200);
            List<String> matchData = new ArrayList(matches.size());
            boolean[] results = new boolean[matches.size()];
            int i = 0;
            for (Match match : matches) {
                matchData.add(match.getData());
                
                results[i++] = match.isEqual();
            }
            
            final String token = PGKeys.randomCode();
            FreakingEggs.newFE(uid, token, results);
            
            minigame.decTurn();
            
            Logging.log(new ActionRecord(User.getUser(uid), now, UserAction.BD_FREAKINGEGGS, token, minigame.getTurn()));
            
            return AMFBuilder.make(PGMacro.MNG_TOKEN, token, PGMacro.DATA,
                    AMFBuilder.toAMF(matchData));
        } catch (Exception ex) {
            PGException.pgThrow(ex);
        }
        
        return null;
    }
    
    public Object returnFreakingEggsAction(String uid,
            String token, List<Boolean> results, long now)
    {
        FreakingEggs fe = FreakingEggs.getFE(uid, token);
        PGException.Assert(results.size() <= fe.nTurn(),
                PGError.INVALID_GAME_DATA, "Result size greater than max");
        
        int n = 0;
        while (n < results.size())
        {
            if (results.get(n) != fe.resultAt(n))
            {
                break;
            }
            
            ++n;
        }
        
        // prizing
        CFMiniGame.FreakingEggs conf = PGConfig.inst().miniGame().getFreakingEggs();
        Map<String, Object> prizeData = conf.getPrize(n);
        PGPrize prize = PrizeFactory.getPrize(prizeData, UserAction.KT_FREAKINGEGGS);
        EntityContext context = EntityContext.getContext(uid);
        Object desc = prize.award(context, now);
        context.saveToDB();
        
        Minigame minigame = Minigame.getMinigame(uid);
        int record = PGHelper.toInt(minigame.getData(PGMacro.MNG_FREAKING_EGGS_RECORD));
        if (n > record)
        {
            minigame.setData(PGMacro.MNG_FREAKING_EGGS_RECORD, n);
        }
        
        FreakingEggs.destroy(uid, token);
        
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        uLogger.log(new FreakingEggsRecord(n));
        Logging.log(new ActionRecord(context.getUser(), now, UserAction.KT_FREAKINGEGGS, token, n));
        
        return AMFBuilder.make(PGMacro.MNG_RESULT, n, PGMacro.PRIZE_DESC, desc);
    }
    
    public Object buyMinigameTurnAction(String uid, long now)
    {
        CFMiniGame conf = PGConfig.inst().miniGame();
        Minigame minigame = Minigame.getMinigame(uid);
        int cTurn = minigame.getTurn();
        
        PGException.Assert(cTurn < conf.getMaxStock(),
                PGError.OVER_MNG_STOCK,
                "Over minigame stocks");
        
        UserDailyData dailyData = UserDailyData.getData(uid, now);
        int todayBougth = PGHelper.toInt(dailyData.getData(PGMacro.MNG_BUY_TODAY));
        
        PGException.Assert(todayBougth < conf.getLimitBuyPerDay(),
                PGError.OVER_BUY_MNG_TURN_TODAY,
                "Over minigame turn bougth today");
        
        int reqCoin = conf.getPrice();
        User user = User.getUser(uid);
        PGException.Assert(user.getCoin() >= reqCoin,
                PGError.NOT_ENOUGH_COIN, "");
        
        UserServices.inst().decreaseCoin(user, reqCoin, now, UserAction.MUA_LUOT_MINIGAME);
        user.saveToDB();
        dailyData.incData(PGMacro.MNG_BUY_TODAY, 1);
        minigame.incTurn(1);
        
        Logging.log(new ActionRecord(user, now, UserAction.MUA_LUOT_MINIGAME, 1));
        
        return AMFBuilder.make(PGMacro.MNG_TURN, minigame.getTurn(),
                PGMacro.COIN, user.getCoin());
    }
    
    public Object upgradeInventoryAction(String uid, long now)
    {
        Inventory inv = Inventory.getInventory(uid);
        int cLevel = inv.getLevel();
        int nextLevel = cLevel + 1;
        
        CFUser.Inventory.Level conf = 
                PGConfig.inst().getUser().getInventory().get(nextLevel);
        PGException.Assert(conf != null,
                PGError.INVALID_CONFIG, "Invalid inventory level");
        
        int price = conf.getPrice();
        User user = User.getUser(uid);
        PGException.Assert(user.getCoin() >= price, 
                PGError.NOT_ENOUGH_COIN, "Not enough coin for upgrade inventory");
        UserServices.inst().decreaseCoin(user, price, now, UserAction.NC_TUI_DO);
        
        inv.setLevel(nextLevel);
        
        user.saveToDB();
        inv.saveToDB();
        
        Logging.log(new ActionRecord(user, now,
                UserAction.NC_TUI_DO, nextLevel));
        
        return AMFBuilder.make(PGMacro.LEVEL, nextLevel, PGMacro.COIN, user.getCoin());
    }
    
    public Object findEggs(String uid, String eggKind, long now)
    {
        User user = User.getUser(uid);
        PGException.Assert(user.getLevel() >= PGConfig.inst().findEggs().getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        FindEggs findEggs = FindEggsServices.inst().updateAndGet(user, now);
        
        PGException.Assert(findEggs.getState() == FindEggs.State.CHOOSE_EGGS,
                PGError.INVALID_FENPC_STATE, "");
        PGException.Assert(findEggs.getNextFind() <= now,
                PGError.CANNOT_FIND_EGGS, "Invalid time");
        
        CFFindEggsNPC conf = PGConfig.inst().findEggs();
        CFFindEggsNPC.Egg eggConf = conf.eggs().get(eggKind);
        
        PGException.Assert(eggConf != null,
                PGError.INVALID_EGG, "Invalid egg");
        PGException.Assert(user.getLevel() >= eggConf.getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        
        for (int i = 0; i < FindEggs.getNOptions(); i++) {
            int nEgg = conf.randomNOfEggs(i);
            int goldPrice = Math.max(1, (int) Math.round(PGHelper.rand(
                    eggConf.getMinGold() * nEgg, eggConf.getMaxGold() * nEgg)));
            int coinPrice = Math.max(1, (int) Math.round(PGHelper.rand(
                    eggConf.getMinCoin() * nEgg, eggConf.getMaxCoin() * nEgg)));
            
            findEggs.getOptions()[i].setNOfEgg(nEgg);
            findEggs.getOptions()[i].setGoldPrice(goldPrice);
            findEggs.getOptions()[i].setCoinPrice(coinPrice);
        }
        
        long foundTime = (long) PGHelper.rand(
                eggConf.getMinFindTime(), eggConf.getMaxFindTime());
        foundTime *= 1000L;
        foundTime += now;
        
        findEggs.setState(FindEggs.State.FINDING);
        findEggs.setFindEgg(eggKind);
        findEggs.setFoundTime(foundTime);
        findEggs.setNextFind(Long.MAX_VALUE);
        findEggs.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.TIM_TRUNG,
            eggKind));
        
        return findEggs.buildAMF();
    }
    
    public Object getFoundEggs(String uid, long now)
    {
        User user = User.getUser(uid);
        PGException.Assert(user.getLevel() >= PGConfig.inst().findEggs().getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        
        FindEggs findEggs = FindEggsServices.inst().updateAndGet(user, now);
        return findEggs.buildAMF();
    }
    
    public Object buyFoundEggs(String uid, int op, String paymentType, long now)
    {
        User user = User.getUser(uid);
        PGException.Assert(user.getLevel() >= PGConfig.inst().findEggs().getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        
        FindEggs findEggs = FindEggsServices.inst().updateAndGet(user, now);
        
        PGException.Assert(findEggs.getState() == FindEggs.State.FOUND,
                PGError.INVALID_FENPC_STATE, "Invalid state");
        
        String eggKind = findEggs.getFindEgg();
        FindEggs.EggOption option = findEggs.getOptions()[op];
        int nEgg = option.getnEgg();
        
        CFPaymentType payType = CFPaymentType.parse(paymentType);
        
        Inventory inv = Inventory.getInventory(uid);
        PGException.Assert(payType != CFPaymentType.none ||
                inv.eggStore().nOfEggs() + nEgg <= inv.getMax(),
                PGError.NOT_ENOUGH_INVENTORY_SLOT, "");
        
        boolean haveBuy = false;
        switch (payType)
        {
            case gold:
                PGException.Assert(user.getGold() >= option.getGoldPrice(),
                        PGError.NOT_ENOUGH_GOLD, "");
                QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
                UserServices.inst().decreaseGold(user, qLogger,
                        option.getGoldPrice(), now, UserAction.MUA_TRUNG_NPC);
                haveBuy = true;
                Logging.log(new ActionRecord(user, now, UserAction.MUA_TRUNG_NPC,
                    eggKind, nEgg, "gold", option.getGoldPrice()));
                break;
            case coin:
                PGException.Assert(user.getCoin() >= option.getCoinPrice(),
                        PGError.NOT_ENOUGH_COIN, "");
                UserServices.inst().decreaseCoin(user, option.getCoinPrice(),
                        now, UserAction.MUA_TRUNG_NPC);
                haveBuy = true;
                Logging.log(new ActionRecord(user, now, UserAction.MUA_TRUNG_NPC,
                    eggKind, nEgg, "coin", option.getCoinPrice()));
                break;
            default:
                break;
        }
        
        if (haveBuy)
        {
            inv.eggStore().addEggs(eggKind, nEgg);
            
            QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
            uLogger.log(new BuyFindEggRecord(nEgg));
        }
        
        findEggs.setState(FindEggs.State.COOLDOWN);
        findEggs.setNextFind(now + PGConfig.inst().findEggs().getCooldown());
        
        // check employ time
        if (now >= findEggs.getEndEmployTime())
        {
            findEggs.setState(FindEggs.State.LOCKED);
        }
        findEggs.saveToDB();
        
        user.saveToDB();
        
        return findEggs.buildAMF();
    }
    
    public Object employFindEggsNPC(String uid, String itemID, long now)
    {
        User user = User.getUser(uid);
        PGException.Assert(user.getLevel() >= PGConfig.inst().findEggs().getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        
        CFFindEggsNPC.Item item = PGConfig.inst().findEggs().items().get(itemID);
        PGException.Assert(item != null, PGError.INVALID_CONFIG, "Invlaid item");
        
        FindEggs findEggs = FindEggsServices.inst().updateAndGet(user, now);
        FindEggs.State state = findEggs.getState();
        PGException.Assert(state == FindEggs.State.LOCKED ||
                state == FindEggs.State.COOLDOWN ||
                state == FindEggs.State.CHOOSE_EGGS,
                PGError.INVALID_FENPC_STATE, "Invalid state");
        
        long newEndTime = Math.max(findEggs.getEndEmployTime(), now) + item.getTime();
        PGException.Assert(newEndTime - now < PGConfig.inst().findEggs().getMaxTime(),
                PGError.MAX_FENPC_TIME, "Max employ time");
        
        switch (item.getPayType())
        {
            case gold:
                PGException.Assert(user.getGold() >= item.getPrice(),
                        PGError.NOT_ENOUGH_GOLD, "");
                QuestLogger qLogger = QuestServices.inst().getQuestLogger(uid, now);
                UserServices.inst().decreaseGold(user, qLogger,
                        item.getPrice(), now, UserAction.THUE_NPC_TIM_TRUNG);
                Logging.log(new ActionRecord(user, now, UserAction.THUE_NPC_TIM_TRUNG,
                    itemID, "gold", item.getPrice(), newEndTime));
                break;
            case coin:
                PGException.Assert(user.getCoin() >= item.getPrice(),
                        PGError.NOT_ENOUGH_COIN, "");
                UserServices.inst().decreaseCoin(user, item.getPrice(),
                        now, UserAction.THUE_NPC_TIM_TRUNG);
                Logging.log(new ActionRecord(user, now, UserAction.THUE_NPC_TIM_TRUNG,
                    itemID, "coin", item.getPrice(), newEndTime));
                break;
            default:
                throw new PGException(PGError.INVALID_CONFIG, "Invalid payment");
        }
        
        findEggs.setEndEmployTime(newEndTime);
        if (findEggs.getState() == FindEggs.State.LOCKED)
        {
            findEggs.setState(FindEggs.State.CHOOSE_EGGS);
            findEggs.setNextFind(now);
        }
        findEggs.saveToDB();
        user.saveToDB();
        
        return findEggs.buildAMF();
    }
    
    public Object freeFindEggsNPCFirstTime(String uid, long now)
    {
        User user = User.getUser(uid);
        PGException.Assert(user.getLevel() >= PGConfig.inst().findEggs().getUnlockLevel(),
                PGError.NOT_ENOUGH_LEVEL, "Not enough level");
        
        FindEggs findEggs = FindEggsServices.inst().updateAndGet(user, now);
        FindEggs.State state = findEggs.getState();
        PGException.Assert(state == FindEggs.State.LEVEL_LOCKED,
                PGError.INVALID_FENPC_STATE, "Invalid state, requre level_locked");
        
        long newEndTime = now + PGConfig.inst().findEggs().getFreeTime();
        
        findEggs.setState(FindEggs.State.CHOOSE_EGGS);
        findEggs.setEndEmployTime(newEndTime);
        findEggs.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.MIEN_PHI_NPC_TIM_TRUNG));
        
        return findEggs.buildAMF();
    }
    
    public Object getUrgentQuest(String uid, long now)
    {
        User user = User.getUser(uid);
        UrgentQuestContext uqContext = QuestServices.inst()
                .updateAndGetUrgentQuests(user, now);
        
        return uqContext.buildAMF();
    }
    
    public Object returnUrgentQuestPack(String uid, int qIndx, long now)
    {
        User user = User.getUser(uid);
        UrgentQuestContext uqContext = QuestServices.inst()
                .updateAndGetUrgentQuests(user, now);
        
        PGException.Assert(uqContext.getQuest().getState() == UrgentQuest.State.HAVE_QUEST,
                PGError.INVALID_UQ_STATE, "Invalid state, not have quest");
        
        UQPack pack = UQPack.getUQPack(uid, qIndx);
        PGException.Assert(pack.getState() == QuestState.ACCEPTED,
                PGError.INVALID_UQ_STATE, "Pack state invalid");
        
        QuestChecker packChecker = QuestFactory.getChecker(null, (Map) pack.getAction());
        EntityContext context = EntityContext.getContext(user);
        PGException.Assert(packChecker.isAccept(context),
                PGError.INCOMPLETED_QUEST, "Not have enough resource to complete this quest");
        
        PGPrize prize = PrizeFactory.getPrize(pack.getPrize(), UserAction.TRA_QUEST_KHAN_1_GOI);
        
        packChecker.returnQuest(context);
        Object pzDesc = prize.award(context, now);
        
        pack.setState(QuestState.COMPLETED);
        
        context.saveToDB();
        pack.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.TRA_QUEST_KHAN_1_GOI, qIndx));
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        uLogger.log(new CompleteUQPackRecord());
        
        return AMFBuilder.make(PGMacro.PRIZE_DESC, pzDesc);
    }
    
    public Object returnUrgentQuestTotal(String uid, long now)
    {
        User user = User.getUser(uid);
        UrgentQuestContext uqContext = QuestServices.inst()
                .updateAndGetUrgentQuests(user, now);
        
        PGException.Assert(uqContext.getQuest().getState() == UrgentQuest.State.HAVE_QUEST,
                PGError.INVALID_UQ_STATE, "Invalid state, not have quest");
        
        for (UQPack pack : uqContext.getPacks()) {
            PGException.Assert(pack.getState() == QuestState.COMPLETED,
                    PGError.INCOMPLETED_EVENT, "Has incompleted pack");
        }
        
        PGPrize prize = PrizeFactory.getPrize(uqContext.getQuest().getTotalPrize(),
                UserAction.TRA_QUEST_KHAN_TONG);
        EntityContext context = EntityContext.getContext(uid);
        Object pzDesc = prize.award(context, now);
        
        uqContext.getQuest().setState(UrgentQuest.State.COOLDOWN);
        uqContext.getQuest().setNext(now + PGConfig.inst().uq().getCooldown());
        QuestServices.inst().generateUrgentQuests(user, uqContext);
        
        context.saveToDB();
        uqContext.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.TRA_QUEST_KHAN_TONG));
        QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
        uLogger.log(new CompleteUQTotalRecord());
        
        return AMFBuilder.make(PGMacro.PRIZE_DESC, pzDesc,
                PGMacro.URGENT_QUEST, uqContext.buildAMF());
    }
    
    public Object dismissUrgentQuest(String uid, long now)
    {
        User user = User.getUser(uid);
        UrgentQuestContext uqContext = QuestServices.inst()
                .updateAndGetUrgentQuests(user, now);
        
        PGException.Assert(uqContext.getQuest().getState() == UrgentQuest.State.HAVE_QUEST,
                PGError.INVALID_UQ_STATE, "Invalid state, not have quest");
        
        for (UQPack pack : uqContext.getPacks()) {
            PGException.Assert(pack.getState() != QuestState.COMPLETED,
                    PGError.INCOMPLETED_EVENT, "Has completed pack");
        }
        
        uqContext.getQuest().setState(UrgentQuest.State.COOLDOWN);
        uqContext.getQuest().setNext(now + PGConfig.inst().uq().getCooldown());
        QuestServices.inst().generateUrgentQuests(user, uqContext);
        
        uqContext.saveToDB();
        
        Logging.log(new ActionRecord(user, now, UserAction.HUY_QUEST_KHAN));
        
        return uqContext.buildAMF();
    }
}